import path from 'path'
import { pathToFileURL } from 'url'
import { parentPort, threadId } from 'worker_threads'
import { asyncState, PromiseState } from '../state'
import {
  MESSAGE_CALL,
  MESSAGE_CALLBACK,
  MESSAGE_CALLBACK_RESULT_ERROR,
  MESSAGE_CALLBACK_RESULT_SUCCESS,
  MESSAGE_CALL_ERROR,
  MESSAGE_CALL_SUCCESS,
  MESSAGE_EXIT,
  MESSAGE_EXITED,
  MESSAGE_READY,
  MESSAGE_RESULT_ERROR
} from './types'

if (!parentPort) {
  throw new Error('This module can only be used on a worker-thread')
}

const modules = new Map<string, object | Promise<object>>()
const callbackStates = new Map<number, PromiseState<any>>()

const _import = (id: string) => {
  if (path.isAbsolute(id)) {
    id = pathToFileURL(id).href
  }

  return import(id)
}

const loadModule = async (id: string): Promise<any> => {
  let exports = modules.get(id)
  if (!exports) {
    modules.set(
      id,
      (exports = _import(id).then(_exports => {
        modules.set(id, (exports = _exports))
        return exports
      }))
    )
  }

  return await exports
}

const invokeMethod = async (id: string, method: string, args: any[]) => {
  const exports = await loadModule(id)

  const methodObject = (exports as any)?.[method]
  if (typeof methodObject !== 'function') {
    throw new Error(`Method '${method}' in module '${id}' is not a function.`)
  }

  return methodObject(...args)
}

const {
  getPrototypeOf: ObjectGetPrototypeOf,
  setPrototypeOf: ObjectSetPrototypeOf,
  prototype: ObjectPrototype
} = Object
const { isArray } = Array
const awaitList = new Set<Promise<any>>()
const isLowerLevel = !!process.env.FAST_WORKER_LOWER_LEVEL

let _k = 0
let runnings = 0
let timer: any

const onMessage = (data: any) => {
  let callPromise: Promise<any> | undefined

  switch (data[0]) {
    case MESSAGE_CALL:
      callPromise = (async () => {
        const [_, callKey, id, method, args, callbackArgs] = data
        try {
          ++runnings

          for (let i = 0, length = callbackArgs.length; i < length; ++i) {
            const arg = callbackArgs[i]
            if (typeof arg === 'string') {
              args[i] = (0, eval)(arg)
            } else {
              args[i] = async (...args: any[]) => {
                const callKey = ++_k
                const state = asyncState<any>()
                callbackStates.set(callKey, state)
                parentPort!.postMessage([MESSAGE_CALLBACK, callKey, arg, args])

                return state
              }
            }
          }

          const result = await invokeMethod(id, method, args)
          parentPort!.postMessage([MESSAGE_CALL_SUCCESS, callKey, result])
        } catch (error) {
          parentPort!.postMessage([MESSAGE_CALL_ERROR, callKey, error])
        } finally {
          --runnings
          if (isLowerLevel === true && runnings === 0) {
            timer ??= setTimeout(() => {
              timer = undefined
              if (runnings === 0) {
                process.exit()
              }
            }, 1024)
          }
        }
      })()

      break

    case MESSAGE_CALLBACK_RESULT_SUCCESS:
      {
        const [_, callKey, result] = data
        const state = callbackStates.get(callKey)
        if (state) {
          callbackStates.delete(callKey)
          state.resolve(result)
        }
      }
      break

    case MESSAGE_CALLBACK_RESULT_ERROR:
      {
        const [_, callKey, result] = data
        const state = callbackStates.get(callKey)
        if (state) {
          callbackStates.delete(callKey)

          if (result && typeof result === 'object') {
            switch (ObjectGetPrototypeOf(result)) {
              case null:
              case ObjectPrototype:
                ObjectSetPrototypeOf(result, Error.prototype)

                break
            }
          }

          state.reject(result)
        }
      }
      break

    case MESSAGE_EXIT:
      parentPort!.off('message', onMessage)
      parentPort!.off('messageerror', onMessageerror)
      Promise.all(awaitList).finally(() => {
        parentPort!.postMessage([MESSAGE_EXITED])
        parentPort!.close()
        process.exit()
      })
      break

    default:
      throw new TypeError(`Unexpected request from parent process: ${data[0]}`)
  }

  if (callPromise) {
    callPromise.finally(() => {
      awaitList.delete(callPromise!)
    })
    awaitList.add(callPromise)
  }
}

const onMessageerror = (error: Error) => {
  const { data } = error as any
  if (isArray(data)) {
    const [request, callKey] = data
    parentPort!.postMessage([request | MESSAGE_RESULT_ERROR, callKey, JSON.parse(JSON.stringify(error))])
  }
}

parentPort.on('message', onMessage)
parentPort.on('messageerror', onMessageerror)
parentPort.start()
parentPort.postMessage([MESSAGE_READY])
